package com.googlecode.groovy_toy_orm

import groovy.sql.Sql
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import java.sql.ResultSet

import static com.googlecode.groovy_toy_orm.internal.Util.asGString



interface Builder<T> {
    T build()
}

interface WhereClauseBuilder extends Builder<QueryExecutor> {
    WhereClauseBuilder and(Condition c)

    WhereClauseBuilder and(String field, ConditionOp op, def value)

    WhereClauseBuilder or(Condition c)

    WhereClauseBuilder or(String field, ConditionOp op, def value)

    WhereClauseBuilder andNot(Condition c)

    WhereClauseBuilder orNot(Condition c)

    WhereClauseBuilder openBrackets()

    WhereClauseBuilder closeBrackets()

}
interface QueryBuilder extends Builder<QueryExecutor> {
    QueryBuilder field(String f)

    /**
     *  Adds one column
     * @param expression something like count(*), $field1+$field2, etc. $field expressions will be parsed automatically (will work ONLY with simple fields)
     * @param alias - how will it be mapped
     * @return
     */

    public Map<String,String> getColumnAliases()

    public QueryBuilder expression(String expression, alias)

    public QueryBuilder distinct()

    public QueryBuilder allFields()

    public QueryBuilder fieldList(List<String> fields)

    public WhereClauseBuilder where()

    public WhereClauseBuilder where(Condition c)

    public WhereClauseBuilder where(String field, ConditionOp op, def value)
}

interface QueryExecutor {
    List<Map> execute()

    List<Map> executeRaw()

    Map firstRow()

    Map firstRowRaw()
}

class FromClauseBuilder {
    SingleDbTableMapper mainMapper

    Set<SingleDbTableMapper> mappers
    SingleDbQueryBuilder queryBuilder

    FromClauseBuilder(SingleDbQueryBuilder qb) {
        this.queryBuilder = qb
        mappers = new HashSet<SingleDbTableMapper>()
        mappers.addAll(qb.getMapper().getMappers())
        mainMapper = qb.mapper.mappers.first()
    }

    GString buildFromClause() {
        //assert  queryBuilder.usedMappers.size() == 1, "Can't build clause for more than one database! You should try using multiBuild() function instead of build()"

        String clause = " from ${mainMapper.fqTableName}"

        //We ALWAYS query ALL tables
        List<SingleDbTableMapper> mappers = queryBuilder.mapper.mappers

        mappers.each() {mapper ->
            if (mapper != mainMapper) {
                String keyField = mapper.getKeyField()
                MapperBinding binding = queryBuilder.mapper.getBinding(mapper)

                // bind key column

                if (!mapper.isMultiColumn(keyField)) {
                    clause = clause + " ${binding.joinType.getSql()} join ${mapper.fqTableName} on ("

                    boolean first = true
                    //If keyField is not defined in higher priority mappers, we simply omit binding key field - instead conditions from binding are used only

                    if (queryBuilder.mapper.findMapper(keyField) != mapper) {
                        clause = clause + "${mapper.getFqSourceName(keyField)} = ${queryBuilder.mapper.getFqSourceName(keyField)}"
                        first = false
                    }
//                    else {
//                       // println "No default bindings?"
//                    }

                    binding.conditionsList.each {
                        if (!first) {
                            clause = "${clause} and ${it.buildSql(mapper)}"
                        } else {
                            clause = "${clause} ${it.buildSql(mapper)}"
                            first = false
                        }
                    }
                    clause = clause + ")"
                } else {
                    throw new UnsupportedOperationException("Can't bind table by multi-column key field ${keyField} :(.")
                }


            }
        }
        asGString(clause)
    }
}


abstract class AbstractQueryBuilder implements QueryBuilder {

}


//@SuppressWarnings("UnnecessaryQualifiedReference")
class SingleDbQueryBuilder implements QueryBuilder {
    MultiTableMapper mapper
    Map<String, String> columnAliases = [:]

    protected Sql getDb() {
        return (mapper.mappers.first()).dbGetter()
    }

    Closure<SingleDbTableMapper> mapperGetter = { String field = null ->
        mapper.findMapper(field)

    }

    Set<SingleDbTableMapper> usedMappers = []
    Set<String> usedFields = []
    Set<String> usedSourceFqNames = []

    GString getQueryBase() {
        distinct ? asGString("select distinct ") : asGString("select ")
    }


    boolean distinct = false
    GString columnsList = GString.EMPTY

    SingleDbQueryBuilder(SingleDbTableMapper mapper, Class beanClass) {
        this.mapper = new MultiTableMapper([mapper], beanClass)
    }

    SingleDbQueryBuilder(MultiTableMapper mapper) {
        this.mapper = mapper
    }



    SingleDbQueryBuilder(List<SingleDbTableMapper> mappers, Class beanClass) {
        this.mapper = new MultiTableMapper(mappers, beanClass)
    }


    protected def void registerFieldIfNeeded(String field) {
        if (!usedFields.contains(field)) {
            usedFields.add(field)
            usedMappers.add(mapperGetter(field))
        }
    }

    @Override
    public SingleDbQueryBuilder field(String field) {
        registerFieldIfNeeded(field)
        SingleDbTableMapper mapper = mapperGetter(field)

        List<String> fqNames = new ArrayList(mapper.getFqSourceNames(field).values())

        //Don't add field if it's already mapped!
        fqNames.removeAll(usedSourceFqNames)

        //If nothing to add - exit
        if (!fqNames) return this

        usedSourceFqNames.addAll(fqNames)

        if (!(columnsList == GString.EMPTY)) {
            columnsList = columnsList.plus(asGString(" , "))
        }

        columnsList = columnsList.plus(asGString(fqNames.join(" ,"))).plus(asGString(" "))
        this
    }

    @Override
    QueryBuilder expression(String expression, alias) {
        throw new UnsupportedOperationException("Not implemented yet")
    }

    @Override
    QueryBuilder distinct() {
        distinct = true
        this
    }

    @Override
    public SingleDbQueryBuilder fieldList(List<String> fields) {
        fields.each {
            registerFieldIfNeeded(it)
            field(it)
        }
        this
    }

    @Override
    public SingleDbQueryBuilder allFields() {
        List<String> resColumns = []
        List<String> usedColumns = []

        mapper.allFields.each { Field f ->
            Map fqNames = mapper.getFqSourceNames(f.name)
            f.getFrom().each {
                if (usedColumns.contains(it)) {
                    columnAliases.put(it as String, "${f.simpleName}_${it}" as String)
                    resColumns << fqNames.get(it) + " as ${columnAliases.get(it)}"
                } else {
                    resColumns << fqNames.get(it)
                    usedColumns << it
                }
            }
        }

        columnsList = columnsList.plus(resColumns.join(", "))
        usedFields = mapper.getAllFields().collect { it.name }
        usedMappers = new ArrayList(mapper.mappers)
        this
    }

    @Override
    SingleDbWhereClauseBuilder where() {
        new SingleDbWhereClauseBuilder(this)
    }

    @Override
    SingleDbWhereClauseBuilder where(Condition c) {
        new SingleDbWhereClauseBuilder(this).and(c)
    }

    @Override
    SingleDbWhereClauseBuilder where(String field, ConditionOp op, def value) {
        new SingleDbWhereClauseBuilder(this).and(new Condition(field: field, op: op, value: value))
    }

    private GString getFromClause() {
        FromClauseBuilder fB = new FromClauseBuilder(this)
        fB.buildFromClause()
    }

    protected GString getBaseClause() {
        queryBase.plus(columnsList).plus(getFromClause())
    }

    @Override
    QueryExecutor build() {
        new SingleDbQueryExecutor(this, getBaseClause())
    }


}


class SingleDbWhereClauseBuilder implements WhereClauseBuilder {
    SingleDbQueryBuilder qBuilder
    List<Condition> postFilters = []
    GString whereClause = asGString(" where 1=1 ")
    int bracketsCounter = 0

    SingleDbWhereClauseBuilder(SingleDbQueryBuilder builder) {
        this.qBuilder = builder
        //Add main table conditions
        List<Condition> mainTableConditions = builder.mapper.getBinding(qBuilder.mapper.mappers.first()).conditionsList
        mainTableConditions.each {
            and(it)
        }

    }

    @Override
    public SingleDbWhereClauseBuilder and(Condition c) {
        qBuilder.registerFieldIfNeeded(c.field)
        if (!qBuilder.mapper.isCalculated(c.field)) {
            whereClause = whereClause.plus(asGString(" and ")).plus(c.buildSql(qBuilder.mapperGetter(c.field)))
        } else {
            postFilters << c
        }
        this
    }

    @Override
    public SingleDbWhereClauseBuilder and(String field, ConditionOp op, def value) {
        qBuilder.registerFieldIfNeeded(field)
        and(new Condition(field: field, op: op, value: value))
    }

    public SingleDbWhereClauseBuilder openBrackets() {
        bracketsCounter++
        whereClause = whereClause.plus(asGString(" ( "))
        this
    }

    public SingleDbWhereClauseBuilder closeBrackets() {
        bracketsCounter--
        whereClause = whereClause.plus(asGString(" ) "))
        this
    }

    private void closeAllBrackets() {
        int mx = bracketsCounter
        for (int i = 0; i < mx; i++)
            closeBrackets()
    }



    @Override
    public SingleDbWhereClauseBuilder or(Condition c) {
        if (qBuilder.mapper.isCalculated(c.field))
            throw new UnsupportedOperationException("Can't use 'or' with calculated field!")

        qBuilder.registerFieldIfNeeded(c.field)
        whereClause = whereClause.plus(asGString(" or ")).plus(c.buildSql(qBuilder.mapperGetter(c.field)))
        this
    }

    @Override
    public SingleDbWhereClauseBuilder or(String field, ConditionOp op, def value) {
        qBuilder.registerFieldIfNeeded(field)
        or(new Condition(field: field, op: op, value: value))
    }

    @Override
    public SingleDbWhereClauseBuilder andNot(Condition c) {
        qBuilder.registerFieldIfNeeded(c.field)
        whereClause = whereClause.plus(asGString(" and not ")).plus(c.buildSql(qBuilder.mapperGetter(c.field)))
        this
    }

    @Override
    public SingleDbWhereClauseBuilder orNot(Condition c) {
        qBuilder.registerFieldIfNeeded(c.field)
        whereClause = whereClause.plus(asGString(" or not ")).plus(c.buildSql(qBuilder.mapperGetter(c.field)))
        this
    }


    QueryExecutor build() {
        closeAllBrackets()
        GString query = qBuilder.getBaseClause().plus(whereClause)
        new SingleDbQueryExecutor(qBuilder, query, postFilters)
    }

}


class SingleDbQueryExecutor implements QueryExecutor {
    SingleDbQueryBuilder qBuilder
    GString query
    List<Condition> postFilters
    private static Logger logger = LoggerFactory.getLogger("MainLogger")


    SingleDbQueryExecutor(SingleDbQueryBuilder qBuilder, GString query, List<Condition> postFilters = []) {
        this.query = query
        this.qBuilder = qBuilder
        this.postFilters = postFilters

        logger.debug("Executor created for query ${query} with additional filters ${postFilters}")

    }

    private Sql getDb() {
        qBuilder.getDb()
    }


    private boolean matchesFilter(Map rawRow) {
        for (Condition f in postFilters) {
            Field field = qBuilder.mapper.getField(f.field)
            try {
                Object value = field.transform(field, rawRow)
                if (!f.apply(value)) return false
            } catch (NoSuchElementException ignored) {
                return false
            }

        }
        true
    }

    @Override
    List<Map> execute() {
        List<Map> rawRows = executeRaw()
        List rows = []

        rawRows.each() { Map rawRow ->
            if (qBuilder.columnAliases) {
                rows << qBuilder.mapper.transformMap(rawRow, qBuilder.columnAliases)
            } else {
                rows << qBuilder.mapper.transformMap(rawRow)
            }

        }
        rows
    }

    @Override
    /**
     * Note, fields can be Aliased!!!!
     */
    List<Map> executeRaw() {
        List<Map> rawRows = new ArrayList()
        getDb().eachRow(query) { ResultSet row ->
            Map thisRow = row.toRowResult()
            if (matchesFilter(thisRow)) {
                rawRows << thisRow
            }
        }

        rawRows
    }


    @Override
    Map firstRow() {
        if (!postFilters) {
            Map rawRow = getDb().firstRow(query)
            if (qBuilder.columnAliases) {
                return qBuilder.mapper.transformMap(rawRow, qBuilder.columnAliases)
            } else {
                return qBuilder.mapper.transformMap(rawRow)
            }
        } else {
            List<Map> allRaw = executeRaw()
            for (Map rawRow : allRaw) {
                Map processedRow
                if (qBuilder.columnAliases) {
                    processedRow = qBuilder.mapper.transformMap(rawRow, qBuilder.columnAliases)
                } else {
                    processedRow = qBuilder.mapper.transformMap(rawRow)
                }
                if (matchesFilter(processedRow)) return processedRow
            }
            return null
        }
    }

    @Override
    Map firstRowRaw() {
        getDb().firstRow(query)
    }
}


println "Q!"